﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using JelleDruyts.Windows;
using Schedulr.Messages;
using Schedulr.Models;
using Schedulr.Providers;
using Schedulr.ViewModels;

namespace Schedulr.Infrastructure
{
    /// <summary>
    /// Performs common tasks.
    /// </summary>
    public static class Tasks
    {
        #region Upload Pictures

        /// <summary>
        /// Uploads the specified pictures asynchronously.
        /// </summary>
        /// <param name="account">The account for which to upload the pictures.</param>
        /// <param name="pictures">The pictures to upload.</param>
        /// <param name="postUploadCallback">The method to call when the upload has finished.</param>
        /// <returns><see langword="true"/> if pictures are being uploaded in the background upon return, <see langword="false"/> otherwise.</returns>
        public static bool UploadPictures(Account account, ICollection<Picture> pictures, Action postUploadCallback)
        {
            if (account == null)
            {
                throw new ArgumentNullException("account");
            }

            if (pictures == null || pictures.Count == 0)
            {
                Logger.Log("An upload was requested but no files were found to upload.", TraceEventType.Information);
                return false;
            }

            if (!FlickrClient.IsOnline())
            {
                Logger.Log("An upload was requested but Flickr is offline or cannot be reached.", TraceEventType.Warning);
                return false;
            }

            var uploadingPicturesTask = new ApplicationTask("Uploading files", pictures.Count * 10);
            Messenger.Send(new TaskStatusMessage(uploadingPicturesTask));
            var step = 0;

            var worker = new BackgroundWorker();
            worker.DoWork += (sender, e) =>
            {
                var uploadedPictures = new List<Tuple<Picture, DateTime>>();
                var i = 0;
                var flickr = new FlickrClient(account.AuthenticationToken);
                foreach (var picture in pictures)
                {
                    uploadingPicturesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Uploading \"{0}\" ({1} of {2})", picture.Title, ++i, pictures.Count));
                    step += 9;
                    try
                    {
                        var uploaded = flickr.Upload(uploadingPicturesTask, account, picture, bytesUploaded => uploadingPicturesTask.SetProgressForCurrentStep(9 * (double)bytesUploaded / picture.FileSize));
                        if (uploaded)
                        {
                            uploadedPictures.Add(new Tuple<Picture, DateTime>(picture, DateTime.Now));
                        }
                    }
                    catch (Exception exc)
                    {
                        var errorMessage = string.Format(CultureInfo.CurrentCulture, "An error occurred while uploading \"{0}\"", picture.Title);
                        Logger.Log(errorMessage, exc);
                        uploadingPicturesTask.SetError(errorMessage, exc);
                    }
                }
                e.Result = uploadedPictures;
            };
            worker.RunWorkerCompleted += (sender, e) =>
            {
                if (e.Error != null)
                {
                    Logger.Log("An unexpected exception occurred while uploading files", e.Error);
                    uploadingPicturesTask.SetError(e.Error);
                    uploadingPicturesTask.SetComplete("An unexpected exception occurred");
                }
                else
                {
                    var uploadedPictures = (List<Tuple<Picture, DateTime>>)e.Result;
                    try
                    {
                        var newBatchId = 1;
                        if (account.UploadedPictures.Count > 0)
                        {
                            newBatchId = account.UploadedPictures.Max(p => p.BatchId) + 1;
                        }
                        var i = 0;
                        foreach (var pictureInfo in uploadedPictures)
                        {
                            var picture = pictureInfo.Item1;
                            uploadingPicturesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Dequeueing \"{0}\" ({1} of {2})", picture.Title, ++i, uploadedPictures.Count));
                            step++;
                            account.QueuedPictures.Remove(picture);
                            picture.DateUploaded = pictureInfo.Item2;
                            picture.BatchId = newBatchId;
                            account.UploadedPictures.Insert(0, picture);
                        }
                    }
                    finally
                    {
                        Tasks.NormalizeBatches(account.QueuedPictures);
                        uploadingPicturesTask.SetComplete(uploadedPictures.Count.ToCountString("file", null, " uploaded"));
                    }
                }

                if (postUploadCallback != null)
                {
                    postUploadCallback();
                }
            };

            worker.RunWorkerAsync();

            return true;
        }

        #endregion

        #region Add Pictures To Queue

        /// <summary>
        /// Adds the specified pictures to the upload queue asynchronously.
        /// </summary>
        /// <param name="account">The account for which to add the pictures.</param>
        /// <param name="fileNames">The file names of the pictures to load and add.</param>
        /// <param name="pictures">The pictures to add.</param>
        /// <param name="addToSingleBatch">A value that determines if all the pictures should be added to a single batch or if each should go in its own batch.</param>
        public static void AddPicturesToQueue(Account account, ICollection<string> fileNames, ICollection<Picture> pictures, bool addToSingleBatch)
        {
            if (fileNames == null)
            {
                fileNames = new string[0];
            }
            if (pictures == null)
            {
                pictures = new Picture[0];
            }
            var addingPicturesTask = new ApplicationTask("Adding files", (fileNames.Count + pictures.Count) * 10);
            Messenger.Send(new TaskStatusMessage(addingPicturesTask));
            var step = 0;

            var queue = account.QueuedPictures;
            var worker = new BackgroundWorker();
            worker.DoWork += (sender, e) =>
            {
                addingPicturesTask.Status = string.Format(CultureInfo.CurrentCulture, "Adding {0}{1} for account \"{2}\"", (fileNames.Count + pictures.Count).ToCountString("picture"), (addToSingleBatch ? " in a single batch" : string.Empty), account.Name);
                var allPictures = new List<Picture>();
                var i = 0;
                // Take a copy of the queue so it cannot be modified by another thread while looping over it to find already queued pictures.
                var queueCopy = queue.ToArray();
                foreach (var fileName in fileNames)
                {
                    if (queueCopy.Any(p => string.Equals(Path.GetFullPath(fileName), Path.GetFullPath(p.FileName), StringComparison.OrdinalIgnoreCase)))
                    {
                        addingPicturesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Skipping \"{0}\" because it is already queued ({1} of {2})", Path.GetFileName(fileName), ++i, fileNames.Count));
                        Logger.Log(string.Format(CultureInfo.CurrentCulture, "Skipping \"{0}\" because it is already queued", Path.GetFileName(fileName)), TraceEventType.Information);
                    }
                    else
                    {
                        try
                        {
                            addingPicturesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Loading \"{0}\" ({1} of {2})", Path.GetFileName(fileName), ++i, fileNames.Count));
                            var picture = PictureProvider.GetPicture(fileName, account.Settings.RetrieveMetadata, account.Settings.PictureDefaults);
                            allPictures.Add(picture);
                        }
                        catch (Exception exc)
                        {
                            var errorMessage = string.Format(CultureInfo.CurrentCulture, "An error occurred while loading \"{0}\"", fileName);
                            Logger.Log(errorMessage, exc);
                            addingPicturesTask.SetError(errorMessage, exc);
                        }
                    }
                    step += 9;
                }
                foreach (var picture in pictures)
                {
                    addingPicturesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Loading \"{0}\" ({1} of {2})", picture.Title, ++i, pictures.Count));
                    step += 9;
                    allPictures.Add(picture);
                }
                e.Result = allPictures;
            };
            worker.RunWorkerCompleted += (sender, e) =>
            {
                if (e.Error != null)
                {
                    Logger.Log("An unexpected exception occurred while adding files", e.Error);
                    addingPicturesTask.SetError(e.Error);
                    addingPicturesTask.SetComplete("An unexpected exception occurred");
                }
                else
                {
                    var allPictures = (IList<Picture>)e.Result;
                    try
                    {
                        var batchId = 1;
                        if (queue.Count > 0)
                        {
                            batchId = queue.Max(p => p.BatchId) + 1;
                        }
                        var i = 0;
                        foreach (var picture in allPictures)
                        {
                            addingPicturesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Adding \"{0}\" ({1} of {2})", picture.Title, ++i, allPictures.Count));
                            step++;
                            picture.BatchId = batchId;
                            if (!addToSingleBatch)
                            {
                                batchId++;
                            }
                            queue.Add(picture);
                        }
                    }
                    finally
                    {
                        Messenger.Send<PictureQueueChangedMessage>(new PictureQueueChangedMessage(PictureQueueChangedReason.PicturesAdded));
                        addingPicturesTask.SetComplete(allPictures.Count.ToCountString("file", null, " added"));
                    }
                }
            };
            worker.RunWorkerAsync();
        }

        #endregion

        #region Add Files From Monitored Folders

        /// <summary>
        /// Adds any new files discovered in the monitored folders to the queue.
        /// </summary>
        /// <param name="account">The account for which to add files.</param>
        public static void AddFilesFromMonitoredFolders(Account account)
        {
            AddFilesFromMonitoredFolders(new Account[] { account });
        }

        /// <summary>
        /// Adds any new files discovered in the monitored folders to the queue.
        /// </summary>
        /// <param name="accounts">The accounts for which to add files.</param>
        public static void AddFilesFromMonitoredFolders(IList<Account> accounts)
        {
            var addingFilesTask = new ApplicationTask("Discovering new files in monitored folders", accounts.Count);
            Messenger.Send(new TaskStatusMessage(addingFilesTask));
            var step = 0;
            var totalFiles = 0;
            try
            {
                foreach (var account in accounts)
                {
                    addingFilesTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Processing account \"{0}\"", account.Name));
                    var newFiles = new List<string>();
                    var existingFiles = account.QueuedPictures.Union(account.UploadedPictures).Select(p => p.FileName);
                    foreach (var folder in account.Settings.FoldersToMonitor)
                    {
                        var folderMessage = string.Format(CultureInfo.CurrentCulture, "Searching for new files for account \"{0}\" in monitored folder \"{1}\"", account.Name, folder);
                        addingFilesTask.Status = folderMessage;
                        Logger.Log(folderMessage, TraceEventType.Information);
                        try
                        {
                            var discoveredFiles = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories);
                            var candidateFiles = discoveredFiles.Where(f => !new FileInfo(f).Attributes.HasFlag(FileAttributes.Hidden));
                            newFiles.AddRange(candidateFiles.Except(existingFiles, StringComparer.InvariantCultureIgnoreCase));
                        }
                        catch (Exception exc)
                        {
                            Logger.Log(string.Format(CultureInfo.CurrentCulture, "Could not search files in directory \"{0}\": {1}", folder, exc.Message), TraceEventType.Warning);
                        }
                    }
                    var filesToAdd = newFiles.Distinct(StringComparer.InvariantCultureIgnoreCase).OrderBy(f => new FileInfo(f).CreationTime).ToList();
                    var resultMessage = string.Format(CultureInfo.CurrentCulture, "Discovered {0} for account \"{1}\"", filesToAdd.Count.ToCountString("file"), account.Name);
                    addingFilesTask.Status = resultMessage;
                    Logger.Log(resultMessage, TraceEventType.Information);
                    if (filesToAdd.Count > 0)
                    {
                        Tasks.AddPicturesToQueue(account, filesToAdd, null, false);
                    }
                    totalFiles += filesToAdd.Count;
                    step++;
                }
            }
            finally
            {
                addingFilesTask.SetComplete(totalFiles.ToCountString("file", null, " discovered"));
            }
        }

        #endregion

        #region Update Accounts

        /// <summary>
        /// Updates the specified account.
        /// </summary>
        /// <param name="account">The account to update.</param>
        public static void UpdateAccount(AccountViewModel account)
        {
            UpdateAccounts(new AccountViewModel[] { account });
        }

        /// <summary>
        /// Updates the specified accounts.
        /// </summary>
        /// <param name="accounts">The accounts to update.</param>
        public static void UpdateAccounts(IList<AccountViewModel> accounts)
        {
            if (accounts == null || accounts.Count == 0)
            {
                return;
            }

            // Create a copy of the accounts collection so that it cannot be modified while foreaching below.
            accounts = accounts.ToArray();

            // Start a task for updating the accounts.
            var updatingAccountsTask = new ApplicationTask("Updating accounts", accounts.Count * 10);
            Messenger.Send(new TaskStatusMessage(updatingAccountsTask));
            var step = 0;

            var worker = new BackgroundWorker();
            worker.DoWork += (sender, e) =>
            {
                foreach (var account in accounts)
                {
                    updatingAccountsTask.SetProgress(step, "Loading account information for " + account.Account.Name);
                    step += 9;
                    account.UpdatePrepare(updatingAccountsTask);
                }
            };
            worker.RunWorkerCompleted += (sender, e) =>
            {
                if (e.Error != null)
                {
                    Logger.Log("An unexpected exception occurred while updating the accounts", e.Error);
                    updatingAccountsTask.SetError(e.Error);
                    updatingAccountsTask.SetComplete("An unexpected exception occurred");
                }
                else
                {
                    var succeededCount = 0;
                    try
                    {
                        foreach (var account in accounts)
                        {
                            updatingAccountsTask.SetProgress(step, "Processing account information for " + account.Account.Name);
                            step++;
                            var updateSucceeded = account.UpdateCommit(updatingAccountsTask);
                            if (updateSucceeded)
                            {
                                succeededCount++;
                            }
                        }
                    }
                    finally
                    {
                        var message = succeededCount.ToCountString("account", null, " updated");
                        if (succeededCount != accounts.Count)
                        {
                            message += string.Format(CultureInfo.CurrentCulture, ", {0} failed", (accounts.Count - succeededCount));
                        }
                        updatingAccountsTask.SetComplete(message);
                    }
                }
            };
            worker.RunWorkerAsync();
        }

        #endregion

        #region Picture Batch Handling

        /// <summary>
        /// Changes the batch of a group of selected pictures.
        /// </summary>
        /// <param name="selectedPictures">The selected pictures for which to set the new batch.</param>
        /// <param name="allPictures">All pictures in the queue.</param>
        /// <param name="batchId">The batch id to apply to the selected pictures.</param>
        /// <param name="newBatchRequested"><see langword="true"/> if a new batch is requested (so that all subsequent pictures are moved up one batch), <see langword="false"/> otherwise.</param>
        public static void ChangeBatch(IEnumerable<Picture> selectedPictures, IEnumerable<Picture> allPictures, int batchId, bool newBatchRequested)
        {
            if (newBatchRequested)
            {
                // Move all pictures in the same or higher batch one level up.
                foreach (Picture picture in allPictures)
                {
                    if (picture.BatchId >= batchId)
                    {
                        picture.BatchId += 1;
                    }
                }
            }

            // Change the batch.
            foreach (Picture picture in selectedPictures)
            {
                picture.BatchId = batchId;
            }

            NormalizeBatches(allPictures);
        }

        /// <summary>
        /// Normalizes the batches of the specified pictures, i.e. renumbers the batches to start at 1 again and removes any gaps.
        /// </summary>
        /// <param name="pictures">The pictures for which to normalize the batches.</param>
        public static void NormalizeBatches(IEnumerable<Picture> pictures)
        {
            var newBatchId = 0;
            var previousBatchId = -1;
            foreach (var picture in pictures.OrderBy(p => p.BatchId))
            {
                if (picture.BatchId != previousBatchId)
                {
                    previousBatchId = picture.BatchId;
                    newBatchId++;
                }

                picture.BatchId = newBatchId;
            }
            Messenger.Send<PictureQueueChangedMessage>(new PictureQueueChangedMessage(PictureQueueChangedReason.BatchesNormalized));
        }

        #endregion

        #region Shuffle

        /// <summary>
        /// Shuffles the specified pictures.
        /// </summary>
        /// <param name="pictures">The pictures to shuffle.</param>
        /// <param name="addToSingleBatch">Determines if the shuffled pictures should be added to a single batch or if they should each be placed in their own batch.</param>
        public static void Shuffle(ObservableCollection<Picture> pictures, bool addToSingleBatch)
        {
            var totalCount = pictures.Count;
            var shuffleTask = new ApplicationTask("Shuffling files", totalCount);
            Messenger.Send(new TaskStatusMessage(shuffleTask));
            var step = 0;
            try
            {
                var random = new Random();
                for (var i = totalCount - 1; i >= 0; i--)
                {
                    shuffleTask.SetProgress(step, string.Format(CultureInfo.CurrentCulture, "Shuffling file {0} of {1}", step + 1, totalCount));
                    pictures.Move(i, random.Next(i + 1));
                    pictures[i].BatchId = (addToSingleBatch ? 1 : i + 1);
                    step++;
                }
            }
            finally
            {
                shuffleTask.SetComplete("Shuffled " + totalCount.ToCountString("file"));
            }
            Messenger.Send<PictureQueueChangedMessage>(new PictureQueueChangedMessage(PictureQueueChangedReason.PicturesShuffled));
        }

        #endregion

        #region Check For Updates

        /// <summary>
        /// Checks for application updates.
        /// </summary>
        /// <param name="newVersionAvailableCallback">The callback method to invoke if there is a new version available, passing in the latest version and the download URL.</param>
        public static void CheckForUpdates(Action<Version, Uri> newVersionAvailableCallback)
        {
            // Don't bother checking if nobody wants to know.
            if (newVersionAvailableCallback != null)
            {
                var worker = new BackgroundWorker();
                worker.DoWork += (sender, e) =>
                {
                    // Check for the latest released version on a background thread.
                    if (CodePlexClient.IsOnline())
                    {
                        Uri downloadUrl;
                        var latestVersion = CodePlexClient.GetLatestReleasedVersion("schedulr", out downloadUrl);
                        if (latestVersion != null && latestVersion > App.FullVersion)
                        {
                            // If the latest released version is newer than the current, return the version and URL.
                            e.Result = new Tuple<Version, Uri>(latestVersion, downloadUrl);
                        }
                    }
                };
                worker.RunWorkerCompleted += (sender, e) =>
                {
                    if (e.Error != null)
                    {
                        Logger.Log("An unexpected exception occurred while checking for updates", e.Error);
                    }
                    else
                    {
                        if (e.Result != null)
                        {
                            var result = (Tuple<Version, Uri>)e.Result;
                            newVersionAvailableCallback(result.Item1, result.Item2);
                        }
                    }
                };

                worker.RunWorkerAsync();
            }
        }

        #endregion
    }
}